package trading_strategy.orders.simulation;

import org.apache.log4j.Logger;
import trading_strategy.events.*;
import trading_strategy.instruments.ListedInstrument;
import trading_strategy.orders.*;

import java.util.HashMap;
import java.util.Map;

/**
 * Simulated exchange that deals with incoming orders and matches them against either the trades or the order book
 * <p/>
 * Created by IntelliJ IDEA.
 * User: gderoujoux
 * Date: 19 mai 2010
 * Time: 16:14:51
 * To change this template use File | Settings | File Templates.
 */
public class SimulatedExchange implements EventListener, IInstrumentExchange {
    private static final Logger LOGGER = Logger.getLogger(SimulatedExchange.class);

    public static void clear() {
        exchanges.clear();
    }

    /**
     * Packages a limit order whose executed qty is also accounted for
     * Allowing to simutae execution against virtual exchange
     */
    class SimulatedLimitOrder {
        int executedQty = 0;
        boolean stopLossOccurred = false;
        LimitOrder order;

        public SimulatedLimitOrder(LimitOrder o) {
            this.order = o;
        }

        public int getQty() {
            return order.getQty() - executedQty;
        }

        public double getLimit() {
            if (stopLossOccurred && order instanceof LimitWithStopLossOrder) {
                return ((LimitWithStopLossOrder) order).getStopLoss();
            }
            return order.getLimit();
        }

        public Order getRealOrder() {
            return order;
        }

        public Double getStopLossLimit() {
            if (order instanceof LimitWithStopLossOrder) {
                return ((LimitWithStopLossOrder) order).getStopLoss();
            }
            return null;
        }

        public void triggerStopLoss() {
            stopLossOccurred = true;
        }

        public void updateOrder(LimitOrder order) {
            this.order = order;
        }
    }

    OrderBookUpdate currentOrderBook; // Current order book on instrument
    Map<Object, SimulatedLimitOrder> limitOrders = new HashMap<Object, SimulatedLimitOrder>(); // All limit orders currently in market

    static MarketIdProvider idProvider = new MarketIdProvider();
    static final Map<ListedInstrument, SimulatedExchange> exchanges = new HashMap<ListedInstrument, SimulatedExchange>();

    private SimulatedExchange(ListedInstrument instrument) {
        EventManager.getInstance().subscribe(this, instrument.getTradeEventProvider());
        EventManager.getInstance().subscribe(this, instrument.getOrderBookProvider());
    }

    public static SimulatedExchange getExchange(ListedInstrument instrument) {
        synchronized (exchanges) {
            SimulatedExchange se = exchanges.get(instrument);
            if (se == null) {
                se = new SimulatedExchange(instrument);
                exchanges.put(instrument, se);
            }
            return se;
        }
    }

    private synchronized void onIOCOrder(IOCLimitOrder order) {
        final SimulatedLimitOrder so = new SimulatedLimitOrder(order);
        executeOrderAgainstOrderBook(so, order.getCreationTimestamp());

        if (so.getQty() != 0) {
            EventManager.getInstance().getDispatcher(EventManager.REPLAY_DISPATCHER).addEvent(new AsyncCancelled(so, order.getCreationTimestamp()));
        }
    }

    private synchronized void onLimitOrder(LimitOrder order) {
        if (order instanceof IOCLimitOrder) {
            onIOCOrder((IOCLimitOrder) order);
        } else {
            final SimulatedLimitOrder simuOrder = new SimulatedLimitOrder(order);
            limitOrders.put(idProvider.nextMarketId(), simuOrder);
            executeOrderAgainstOrderBook(simuOrder, order.getCreationTimestamp()); // Try to execute against current order book
            if (simuOrder.getQty() > 0) {
                try {
                    simuOrder.getRealOrder().getAutomaton().onOrderState(SingleOrderAutomaton.State.ON_MARKET);
                } catch (OrderException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Override
    public void onNewOrder(Order order) {
        if (order instanceof LimitOrder) {
            onLimitOrder((LimitOrder) order);
        } else {
            throw new IllegalArgumentException("Order type not supported");
        }
    }

    @Override
    public void onModifyOrder(Order order) {
        if (order instanceof LimitOrder) {
            final SimulatedLimitOrder so = limitOrders.get(order.getMarketId());
            if (so != null) {
                so.updateOrder((LimitOrder) order);
                try {
                    so.getRealOrder().getAutomaton().onOrderState(SingleOrderAutomaton.State.ON_MARKET);
                } catch (OrderException e) {
                    e.printStackTrace();
                }
            } else {
                throw new IllegalStateException("Cannot modify an order which does not exist");
            }
        } else {
            throw new IllegalArgumentException("Order type not supported");
        }
    }

    @Override
    public void onCancelOrder(Order order) {
        final SimulatedLimitOrder so = limitOrders.get(order.getMarketId());
        if (so != null) {
            try {
                so.getRealOrder().getAutomaton().onOrderState(SingleOrderAutomaton.State.CANCELLED);
            } catch (OrderException e) {
                e.printStackTrace();
            }
        }
    }

    private void executeOrderAgainstOrderBook(final SimulatedLimitOrder order, long ts) {
        if (currentOrderBook == null) {
            return;
        }
        int remainingQty = Math.abs(order.getQty());
        OrderBook book = currentOrderBook.getBook();
        if (order.getQty() > 0 && book.getAsk(0) <= order.getLimit()) {
            for (int i = 0; remainingQty > 0 && i < book.getAskDepth(); i++) {
                final double askPrm = book.getAsk(i);
                if (askPrm <= order.getLimit()) {
                    // Some execution will take place at this level
                    final int executedQty = Math.min(book.getAskQty(i), remainingQty);
                    remainingQty -= executedQty;
                    order.executedQty += executedQty;
                    EventManager.getInstance().getDispatcher(EventManager.REPLAY_DISPATCHER).addEvent(new AsyncExecution(order, executedQty, askPrm, ts));
                } else {
                    break;
                }
            }
        } else if (order.getQty() < 0 && book.getBid(0) >= order.getLimit()) {
            for (int i = 0; remainingQty > 0 && i < book.getBidDepth(); i++) {
                final double bidPrm = book.getBid(i);
                if (bidPrm >= order.getLimit()) {
                    // Some execution will take place at this level
                    int executedQty = Math.min(book.getBidQty(0), remainingQty);
                    remainingQty -= executedQty;
                    order.executedQty -= executedQty;

                    EventManager.getInstance().getDispatcher(EventManager.REPLAY_DISPATCHER).addEvent(new AsyncExecution(order, -executedQty, bidPrm, ts));
                } else {
                    break;
                }
            }
        }
    }

    @Override
    public synchronized void onMarketEvent(AbstractMarketEvent event) {
        if (event instanceof OrderBookUpdate) {
            OrderBookUpdate obu = (OrderBookUpdate) event;
            currentOrderBook = obu;

            for (SimulatedLimitOrder lo : limitOrders.values()) {
                // Check whether execution take place or not
                executeOrderAgainstOrderBook(lo, event.getTs());
            }
        } else if (event instanceof PublicTradeUpdate) {
            PublicTradeUpdate ptu = (PublicTradeUpdate) event;

            for (SimulatedLimitOrder lo : limitOrders.values()) {
                // Check whether execution take place or not
                if (lo.getQty() > 0) {
                    if (lo.getStopLossLimit() != null && lo.getStopLossLimit() <= ptu.getPrice()) {
                        lo.triggerStopLoss();
                        executeOrderAgainstOrderBook(lo, event.getTs());
                    }
                    if (ptu.getPrice() <= lo.getLimit()) {
                        final int execQty = Math.min(Math.abs(lo.getQty()), ptu.getQty());
                        lo.executedQty += execQty;
                        EventManager.getInstance().getDispatcher(EventManager.REPLAY_DISPATCHER).addEvent(new AsyncExecution(lo, execQty, ptu.getPrice(), event.getTs()));
                    }
                } else if (lo.getQty() < 0) {
                    if (lo.getStopLossLimit() != null && lo.getStopLossLimit() >= ptu.getPrice()) {
                        lo.triggerStopLoss();
                        executeOrderAgainstOrderBook(lo, event.getTs());
                    }
                    if (ptu.getPrice() >= lo.getLimit()) {
                        final int execQty = -Math.min(Math.abs(lo.getQty()), ptu.getQty());
                        lo.executedQty += execQty;
                        EventManager.getInstance().getDispatcher(EventManager.REPLAY_DISPATCHER).addEvent(new AsyncExecution(lo, execQty, ptu.getPrice(), event.getTs()));
                    }
                }
                executeOrderAgainstOrderBook(lo, event.getTs());
            }
        }
    }

    static class MarketIdProvider {
        int currentMarketId = 1;

        Object nextMarketId() {
            synchronized (this) {
                return (currentMarketId++);
            }
        }
    }

    private static class AsyncExecution extends EventDispatcher.TimedRunnable {
        private final SimulatedLimitOrder order;
        private final int executedQty;
        private final double price;

        public AsyncExecution(SimulatedLimitOrder order, int executedQty, double price, long ts) {
            super(ts);
            this.order = order;
            order.getRealOrder().setMatchingTimestamp(ts);
            this.executedQty = executedQty;
            this.price = price;
        }

        @Override
        public void run() {
            try {
                order.getRealOrder().getAutomaton().onOrderState(SingleOrderAutomaton.State.EXECUTED);
                order.getRealOrder().getAutomaton().onOrderExecuted(executedQty, price, order.getRealOrder());
            } catch (OrderException e) {
                e.printStackTrace();
            }
        }
    }

    private static class AsyncCancelled extends EventDispatcher.TimedRunnable {
        private final SimulatedLimitOrder order;

        public AsyncCancelled(SimulatedLimitOrder order, long ts) {
            super(ts);
            this.order = order;
            order.getRealOrder().setMatchingTimestamp(ts);
        }

        @Override
        public void run() {
            try {
                order.getRealOrder().getAutomaton().onOrderState(SingleOrderAutomaton.State.CANCELLED);
            } catch (OrderException e) {
                e.printStackTrace();
            }
        }
    }
}
